using System;
using System.Collections.Generic;
using System.Text;
using System.Web.Security;
using Mani.Security.Rules.AuthorizationDataTableAdapters;
using System.Data;
using System.Collections.ObjectModel;
using Mani.Security.Rules;

namespace Mani.Security {


    public interface IRuleProvider {
        bool IsActionPermited( string actionName, string userName );

        void GrantActionToRoles( string actionName, ReadOnlyCollection<string> roles );

        void DenyActionFromRoles( string actionName, ReadOnlyCollection<string> roles );

        void GrantActionToUsers( string actionName, ReadOnlyCollection<string> users );

        void DenyActionFromUsers( string actionName, ReadOnlyCollection<string> users );

        ReadOnlyCollection<Action> GetActionsForRole( string roleName );

        ReadOnlyCollection<Action> GetActionsForUser( string userName );

        bool IsActionAvailable( string actionName );
    }

    public class RuleProvider : IRuleProvider {

        private Guid? applicationId;	

        public Guid ApplicationId {
            get {
                if (applicationId.HasValue)
                    return applicationId.Value;

                using (aspnet_ApplicationsTableAdapter adapter = new aspnet_ApplicationsTableAdapter()) {
                    AuthorizationData.aspnet_ApplicationsDataTable table = adapter.GetDataByLoweredApplicationName( Membership.ApplicationName.ToLower() );
                    if (table.Count == 0)
                        throw new InvalidOperationException( "Specified ApplicationName is not found in your Membership Provider DataSource!" );

                    if (table.Count > 1)
                        throw new InvalidOperationException( "More than one Application found with specified ApplicationName in your Membership Provider DataSource!" );

                    applicationId = table[0].ApplicationId;
                    return table[0].ApplicationId;
                }
            }
        }

        #region IRuleProvider Members

        public bool IsActionPermited( string actionName, string userName ) {
            if (String.IsNullOrEmpty( actionName ))
                return true;

            if (Roles.IsUserInRole( userName, "Administrator" ))
                return true;

            Guid applicationId = ApplicationId;
            Guid actionId;
            Guid userId;

            using (aspnet_ActionsTableAdapter adapter = new aspnet_ActionsTableAdapter()) {
                AuthorizationData.aspnet_ActionsDataTable table = adapter.GetDataByActionName( applicationId, actionName );
                if (table.Count == 0)
                    return true;

                actionId = table[0].ActionId;
            }

            // MANI[Note] Instead of checking for public pages, simply public pages should pass Empty Or Null action Name to this method!
            // so old IsActionPublic method commented.
            //if (IsActionPublic(actionName)) 
            //return true;

            using (aspnet_UsersTableAdapter adapter = new aspnet_UsersTableAdapter()) {
                AuthorizationData.aspnet_UsersDataTable table = adapter.GetDataByUsername( userName, applicationId );
                if (table.Count == 0)
                    return false;

                userId = table[0].UserId;
            }

            // Check User level access first of all
            // if user's access to action implicitly denied or granted
            // there is no need to check his roles' access
            // return whatever it is.
            using (aspnet_UsersActionsTableAdapter adapter = new aspnet_UsersActionsTableAdapter()) {
                AuthorizationData.aspnet_UsersActionsDataTable table = adapter.GetDataByUserId( actionId, userId );
                if (table.Rows.Count > 0)
                    return table[0].Access;
            }


            // Checking User's Roles Access level
            // if one of user's roles has access to action
            // that's enough.
            List<Guid> userRoles = GetUserRoles( userId );
            if (userRoles == null)
                return false; // if user has no role return false

            using (aspnet_RolesActionsTableAdapter adapter = new aspnet_RolesActionsTableAdapter()) {
                foreach (Guid roleGuid in userRoles) {
                    AuthorizationData.aspnet_RolesActionsDataTable table = adapter.GetDataByRoleId( roleGuid, actionId );
                    if (table.Rows.Count > 0 && table[0].Access)
                        return true;
                }
            }

            // non of user's roles has access to this action so return false
            return false;
        }

        public void GrantActionToRoles( string actionName, ReadOnlyCollection<string> roles ) {
            ChangeActionForRoles( GetActionIdByName( actionName ), roles, true );
        }

        public void DenyActionFromRoles( string actionName, ReadOnlyCollection<string> roles ) {
            ChangeActionForRoles( GetActionIdByName( actionName ), roles, false );
        }

        public void GrantActionToUsers( string actionName, ReadOnlyCollection<string> users ) {
            ChangeActionForUsers( GetActionIdByName( actionName ), users, true );
        }

        public void DenyActionFromUsers( string actionName, ReadOnlyCollection<string> users ) {
            ChangeActionForUsers( GetActionIdByName( actionName ), users, false );
        }

        public ReadOnlyCollection<Action> GetActionsForRole( string roleName ) {
            List<Action> output = new List<Action>();
            Guid roleId;

            using (aspnet_RolesTableAdapter adapter = new aspnet_RolesTableAdapter()) {
                AuthorizationData.aspnet_RolesDataTable table = adapter.GetDataByRoleName( roleName, ApplicationId );
                if (table.Count != 0)
                    roleId = table[0].RoleId;
                else
                    throw new ArgumentException( "oOops! Unable to find specified role name: " + roleName );
            }

            using (aspnet_RolesActionsTableAdapter adapter = new aspnet_RolesActionsTableAdapter()) {
                AuthorizationData.aspnet_RolesActionsDataTable table = adapter.GetActionsForRole( roleId );
                if (table.Count != 0) {
                    foreach (AuthorizationData.aspnet_RolesActionsRow row in table) {
                        if ( row.Access )
                            output.Add(Action.Load(row.ActionId));
                    }
                }
            }

            return output.AsReadOnly();
        }


        public ReadOnlyCollection<Action> GetActionsForUser( string userName ) {
            Guid userId;
            List<Guid> roleIds = new List<Guid>();
            List<Action> output = new List<Action>();

            using (aspnet_UsersTableAdapter adapter = new aspnet_UsersTableAdapter()) {
                AuthorizationData.aspnet_UsersDataTable table = adapter.GetDataByUsername( userName, ApplicationId );
                if (table.Count == 0)
                    throw new ArgumentException( "oOops! Unable to GetActionsForUser, Invalid username specified:" + userName );

                userId = table[0].UserId;
            }

            using (aspnet_UsersInRolesTableAdapter adapter = new aspnet_UsersInRolesTableAdapter()) {
                AuthorizationData.aspnet_UsersInRolesDataTable table = adapter.GetDataByUserId( userId );
                foreach (AuthorizationData.aspnet_UsersInRolesRow row in table) {
                    roleIds.Add( row.RoleId );
                }
            }

            using (aspnet_RolesActionsTableAdapter adapter = new aspnet_RolesActionsTableAdapter()) {
                foreach (Guid guid in roleIds) {
                    AuthorizationData.aspnet_RolesActionsDataTable table = adapter.GetActionsForRole( guid );
                    if (table.Count != 0)
                        output.Add( Action.Load( table[0].ActionId ) );
                }
            }

            using (aspnet_UsersActionsTableAdapter adapter = new aspnet_UsersActionsTableAdapter()) {
                AuthorizationData.aspnet_UsersActionsDataTable table = adapter.GetActionsForUser( userId );
                if (table.Count != 0) {
                    foreach (AuthorizationData.aspnet_UsersActionsRow row in table)
                        output.Add( Action.Load( row.ActionId ) );
                }
            }

            return output.AsReadOnly();
        }

        public bool IsActionAvailable( string actionName ) {
            using (aspnet_ActionsTableAdapter adapter = new aspnet_ActionsTableAdapter()) {
                AuthorizationData.aspnet_ActionsDataTable table = adapter.GetDataByActionName( ApplicationId, actionName );
                return (table.Count > 0);
            }
        }

        #endregion

        #region private

        private void ChangeActionForUsers( Guid actionId, ReadOnlyCollection<string> users, bool access) {
            List<Guid> usersGuids = new List<Guid>();

            using (aspnet_UsersTableAdapter adapter = new aspnet_UsersTableAdapter()) {
                foreach (string s in users) {
                    AuthorizationData.aspnet_UsersDataTable table = adapter.GetDataByUsername( s, ApplicationId );
                    if(table.Count == 0)
                        throw new ArgumentException( "oOops! Unable to ChangeRuleForUsers, Invalid username specified." );

                    usersGuids.Add( table[0].UserId );
                }
            }

            using (aspnet_UsersActionsTableAdapter adapter = new aspnet_UsersActionsTableAdapter()) {
                foreach (Guid UserId in usersGuids) {
                    AuthorizationData.aspnet_UsersActionsDataTable table = adapter.GetDataByUserId( actionId, UserId );
                    if (table.Count == 0)
                        adapter.Insert( actionId, UserId, access );
                    else {
                        table[0].Access = access;
                        adapter.Update( table[0] );
                    }
                }
            }
        }

        private void ChangeActionForRoles( Guid actionId, ReadOnlyCollection<string> roles, bool access ) {
            List<Guid> rolesGuids = new List<Guid>();

            using (aspnet_RolesTableAdapter adapter = new aspnet_RolesTableAdapter()) {
                foreach (string s in roles) {
                    AuthorizationData.aspnet_RolesDataTable table = adapter.GetDataByRoleName( s, ApplicationId );
                    if (table.Count == 0)
                        throw new ArgumentException( "oOops! Unable to ChangeRuleForRoles, Invalid rolename specified." );

                    rolesGuids.Add( table[0].RoleId );
                }
            }

            using (aspnet_RolesActionsTableAdapter adapter = new aspnet_RolesActionsTableAdapter()) {
                foreach (Guid RoleId in rolesGuids) {
                    AuthorizationData.aspnet_RolesActionsDataTable table = adapter.GetDataByRoleId( RoleId, actionId );
                    if( table.Count == 0 && access)
                        adapter.Insert( actionId, RoleId, access );
                    else {
                        table[0].Access = access;
                        adapter.Update( table[0] );
                    }
                }
            }
        }

        private List<Guid> GetUserRoles( Guid userId ) {
            List<Guid> roles = new List<Guid>();
            using (aspnet_UsersInRolesTableAdapter adapter = new aspnet_UsersInRolesTableAdapter()) {
                AuthorizationData.aspnet_UsersInRolesDataTable table = adapter.GetDataByUserId( userId );
                foreach (AuthorizationData.aspnet_UsersInRolesRow row in table) {
                    roles.Add( row.RoleId );
                }
            }
            return roles;
        }

        private Guid GetActionIdByName( string actionName ) {
            using (aspnet_ActionsTableAdapter adapter = new aspnet_ActionsTableAdapter()) {
                AuthorizationData.aspnet_ActionsDataTable table = adapter.GetDataByActionName( ApplicationId, actionName );
                if (table.Count == 0)
                    throw new ArgumentException( "Unable to find specified actionName in Membership provider data source!" );

                return table[0].ActionId;
            }
        }

        // MANI[Note] Instead of checking for public pages, simply public pages should pass Empty Or Null action Name.
        // so old IsActionPublic method commented.
        /*private static bool IsActionPublic(string actionName) {
            using (aspnet_ActionsTableAdapter adapter = new aspnet_ActionsTableAdapter()) {
                AuthorizationData.aspnet_ActionsDataTable table = adapter.GetDataByActionName( ApplicationId, actionName );
                if (table.Count == 0)
                    throw new ArgumentException( "Unable to find specified actionName in Membership provider data source!" );

                return table[0].IsPublic;
            }
        }*/

        #endregion
    }
}
